﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BFlatCompile
{
	public class Tokens
	{
		private static readonly Dictionary<string, HashSet<string>> tokenClumps;

		static Tokens()
		{
			Tokens.tokenClumps = new Dictionary<string, HashSet<string>>();
			foreach (string clump in "+= -= *= /= &= |= ^= ++ -- << >> <= >= != == || && ^^".Split(' '))
			{
				HashSet<string> suffixes;
				string prefix = clump[0] + "";
				string suffix = clump.Substring(1);
				if (!tokenClumps.ContainsKey(prefix)) {
					suffixes = new HashSet<string>();
					tokenClumps[prefix] = suffixes;
				}

				tokenClumps[prefix].Add(suffix);
			}
		}

		public override string ToString()
		{
			int start = this.index - 3;
			int end = this.index + 3;
			List<string> output = new List<string>();
			for (int i = start; i <= end; ++i)
			{
				if (i >= 0 && i < this.tokens.Length)
				{
					string value = "\"" + this.tokens[i] + "\"";
					if (i == this.index)
					{
						value = "<" + value + ">";
					}
					output.Add(value);
				}
			}
			return "[Tokens] " + string.Join(", ", output);
		}

		private Token[] tokens;
		private string[] lines;
		private string file;
		private string relativeFile;
		private int index;

		public string FileName { get { return this.file; } }
		public string FileOriginalContents { get; private set; }
		public bool IsBuiltIn { get; private set; }

		public Tokens(string file, string contents, bool builtIn, string relativeFileName)
		{
			this.FileOriginalContents = contents;

			contents = contents
				.Replace("\r\n", "\n")
				.Replace("\r", "\n");
			this.lines = contents.Split('\n');
			this.file = file;
			this.relativeFile = relativeFileName;
			this.Initialize();
			this.index = 0;
			this.IsBuiltIn = builtIn;
		}

		public Token PopInstance()
		{
			Token token = this.PeekToken();
			this.Pop();
			return token;
		}

		// this will still verify that actual is the first expected, but alsoValid will appear in the error message.
		// This is for funny situations where the parser has already verified that the other valid tokens are not present
		// and this was called in an else (or something like that). For example, the for loop initialization code can be
		// delimited by commas, and the loop that parses these out will keep going until it sees a semi-colon, but
		// will call PopExpected on the comma. It would be weird if there was an unexpected token and the error said
		// a comma was expected instead of a semicolon.
		public Token PopExpected(string expected, params string[] alsoValid)
		{
			if (this.index < this.tokens.Length)
			{
				Token actualToken = this.tokens[this.index];
				string actual = actualToken.Value;
				if (expected != actual)
				{
					List<string> expectedString = new List<string>(alsoValid);
					expectedString.Insert(0, expected);

					for (int i = 0; i < expectedString.Count; ++i)
					{
						expectedString[i] = "\"" + expectedString[i] + "\"";
					}

					string expectedMessage;
					if (expectedString.Count == 1)
					{
						expectedMessage = expectedString[0];
					}
					else if (expectedString.Count == 2)
					{
						expectedMessage = expectedString[0] + " or " + expectedString[1];
					}
					else
					{
						expectedString[expectedString.Count - 1] = " or " + expectedString[expectedString.Count - 1];
						expectedMessage = string.Join(", ", expectedString);
					}

					expectedMessage = "Expected " + expectedMessage + ", but found \"" + actual + "\" instead.";

					if (expected == ";")
					{
						expectedMessage += " Did you forget the semicolon on the previous line?";
					}

					throw new ParserException(expectedMessage, actualToken);
				}

				++this.index;
				return actualToken;
			}

			// TODO: use the last coord of the file
			throw new ParserException("Unexpected EOF", this.file, "", -1, -1);
		}

		public Token PreviousToken(int backCount)
		{
			int i = this.index - backCount;
			if (i >= 0 && i < this.tokens.Length)
			{
				return this.tokens[i];
			}
			return null;
		}

		public Token PreviousToken()
		{
			return this.PreviousToken(1);
		}

		public bool PopIfPresent(string expected1, string expected2)
		{
			if (this.Peek() == expected1 && this.Peek(1) == expected2)
			{
				this.Pop();
				this.Pop();
				return true;
			}
			return false;
		}

		public bool PopIfPresent(string expected)
		{
			if (this.index < this.tokens.Length)
			{
				string actual = this.tokens[this.index].Value;
				if (actual == expected)
				{
					++this.index;
					return true;
				}
			}
			return false;
		}

		public bool IsPresent(params string[] values)
		{
			for (int i = 0; i < values.Length; ++i)
			{
				if (this.Peek(i) != values[i])
				{
					return false;
				}
			}
			return true;
		}

		// TODO: go through these functions and make sure the core implementation is the token version and then all .Value on their string wrappers
		public Token PopToken()
		{
			Token token = this.PeekToken();
			this.Pop();
			return token;
		}

		public Token PeekToken()
		{
			if (this.index < this.tokens.Length)
			{
				return this.tokens[this.index];
			}
			return null;
		}

		public string Peek()
		{
			if (this.index < this.tokens.Length)
			{
				return this.tokens[this.index].Value;
			}
			return null;
		}

		public string Peek(int offset)
		{
			if (this.index + offset < this.tokens.Length)
			{
				return this.tokens[this.index + offset].Value;
			}
			return null;
		}

		public string Pop()
		{
			if (this.index < this.tokens.Length)
			{
				return this.tokens[this.index++].Value;
			}
			// TODO: use end of file coords
			throw new ParserException("Unexepcted EOF", this.file, "", -1, -1);
		}

		public string PopAlphaNums()
		{
			string token = this.Pop();
			if (IsAlphaNums(token))
			{
				return token;
			}

			throw new ParserException("Expected alphanumeric value. Found: " + token, this.tokens[this.index - 1]);
		}

		public string PopIdentifier()
		{
			string token = this.Pop();
			if (IsIdentifier(token))
			{
				return token;
			}

			throw new ParserException("Invalid identifier: " + token, this.tokens[this.index - 1]);
		}

		public static bool IsAlphaNums(string value)
		{
			for (int i = 0; i < value.Length; ++i)
			{
				char c = value[i];
				if ((c >= 'a' && c <= 'z') ||
					(c >= 'A' && c <= 'Z') ||
					(c >= '0' && c <= '9') ||
					(c == '_'))
				{
					// yes
				}
				else
				{
					return false;
				}
			}

			return true;
		}

		public static bool IsIdentifier(string value)
		{
			return IsAlphaNums(value) && (value[0] < '0' || value[0] > '9');
		}

		private static bool IsMatch(List<string> tokenValues, int index, string str)
		{
			int strIndex = 0;
			for (int i = index; i < tokenValues.Count; ++i)
			{
				if (strIndex == str.Length)
				{
					return true;
				}
				if (str[strIndex++] + "" != tokenValues[i])
				{
					return false;
				}
			}

			return false;
		}

		private void Initialize()
		{
			List<string> tokenValues = new List<string>();
			List<int> lineNumbers = new List<int>();
			List<int> columnNumbers = new List<int>();

			for (int lineNum = 0; lineNum < this.lines.Length; ++lineNum)
			{
				string line = this.lines[lineNum];
				int col;
				for (col = 0; col < line.Length; ++col)
				{
					string c = "" + line[col];
					tokenValues.Add(c);
					lineNumbers.Add(lineNum);
					columnNumbers.Add(col);
				}

				tokenValues.Add(
					(lineNum < this.lines.Length - 1)
						? "\n"
						: "EOF");
				lineNumbers.Add(lineNum);
				columnNumbers.Add(col);
			}

			List<Token> tokens = new List<Token>();
			bool inLineComment = false;
			bool inMultiLineComment = false;
			bool inSingleQuoteString = false;
			bool inDoubleQuoteString = false;
			bool inAlphaNums = false;
			
			int charCount = tokenValues.Count;
			bool handled = false;
			for (int i = 0; i < charCount; ++i)
			{
				string c = tokenValues[i];
				if (inLineComment)
				{
					if (c == "\n" || c == "EOF")
					{
						inLineComment = false;
					}
					handled = true;
				}
				else if (inMultiLineComment)
				{
					if (c == "*" && Peek(tokenValues, i + 1) == "/")
					{
						inMultiLineComment = false;
						++i;
					}
					else if (c == "EOF")
					{
						throw new TokenizerException(
							"Unexpected EOF: unclosed multiline comment",
							this.lines[lineNumbers[i]],
							lineNumbers[i],
							columnNumbers[i]);
					}
					handled = true;
				}
				else if (inSingleQuoteString || inDoubleQuoteString)
				{
					if (inSingleQuoteString && c == "'")
					{
						inSingleQuoteString = false;
					}
					else if (inDoubleQuoteString && c == "\"")
					{
						inDoubleQuoteString = false;
					}
					else if (c == "EOF")
					{
						throw new TokenizerException(
							"Unexpected EOF: unclosed string",
							this.lines[lineNumbers[i]],
							lineNumbers[i],
							columnNumbers[i]);
					}

					tokens[tokens.Count - 1].AppendValue(c);
					handled = true;
				}
				else if (inAlphaNums)
				{
					if (c == "EOF")
					{
						inAlphaNums = false;
						handled = true;
					}
					else
					{
						char ch = c[0];
						if ((ch >= 'a' && ch <= 'z') ||
							(ch >= 'A' && ch <= 'Z') ||
							(ch >= '0' && ch <= '9') ||
							(ch == '_'))
						{
							tokens[tokens.Count - 1].AppendValue(c);
							handled = true;
						}
						else
						{
							inAlphaNums = false;
							handled = false;
						}
					}

					if (tokens[tokens.Count - 1].Value == "fuzzyPickles")
					{

					}
				}
				else
				{
					handled = false;
				}

				if (!handled)
				{
					if (c == "EOF")
					{
						// do nothing
						handled = true;
					}
					else
					{
						char ch = c[0];
						if (ch == ' ' || ch == '\n' || ch == '\t')
						{
							// do nothing
							handled = true;
						}
						else if (tokenClumps.ContainsKey(c))
						{
							foreach (string suffix in tokenClumps[c])
							{
								if (IsMatch(tokenValues, i + 1, suffix))
								{
									tokens.Add(new Token(c, this.file, this.relativeFile, this.lines, lineNumbers[i], columnNumbers[i]));
									i += suffix.Length;
									tokens[tokens.Count - 1].AppendValue(suffix);
									handled = true;
									break;
								}
							}
						}

						if (!handled)
						{
							if (
							  (ch >= 'a' && ch <= 'z') ||
							  (ch >= 'A' && ch <= 'Z') ||
							  (ch >= '0' && ch <= '9') ||
							  (ch == '_'))
							{
								inAlphaNums = true;
								tokens.Add(new Token(c, this.file, this.relativeFile, this.lines, lineNumbers[i], columnNumbers[i]));
							}
							else if (ch == '/' && Peek(tokenValues, i + 1) == "/")
							{
								inLineComment = true;
							}
							else if (ch == '/' && Peek(tokenValues, i + 1) == "*")
							{
								inMultiLineComment = true;
								++i; // prevent /*/ from registering as open and close
							}
							else if (ch == '"')
							{
								inDoubleQuoteString = true;
								tokens.Add(new Token(c, this.file, this.relativeFile, this.lines, lineNumbers[i], columnNumbers[i]));
							}
							else if (ch == '\'')
							{
								inSingleQuoteString = true;
								tokens.Add(new Token(c, this.file, this.relativeFile, this.lines, lineNumbers[i], columnNumbers[i]));
							}
							else
							{
								tokens.Add(new Token(c, this.file, this.relativeFile, this.lines, lineNumbers[i], columnNumbers[i]));
							}
						}
					}
				}
			}

			this.tokens = tokens.ToArray();
		}

		private string Peek(IList<string> tokens, int i)
		{
			if (i < tokens.Count)
			{
				return tokens[i];
			}
			return null;
		}

		public string PopBase10Digits()
		{
			string value = this.Pop();

			if (value == null) return null;

			for (int i = 0; i < value.Length; ++i)
			{
				if (value[i] < '0') return null;
				if (value[i] > '9') return null;
			}

			return value;
		}
	}
}
